﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using ALDIOrganizer.Properties;

namespace Generator
{
    public partial class Engine
    {
        private const int OUTCOME_COMPLETE = 1;
        private const int OUTCOME_HIT_A_DEAD_END = -1;
        private const int OUTCOME_USER_CANCELLED = -2;

        /// <summary>
        /// the first time, you need to pass endSolution.solution as data.
        /// but it's a recurrent function, so when it's called again (nested),
        /// it will make a local copy every time.
        /// That's why the shift struct has no "rollback" methods.
        /// There are methods for assigning a new employee, but not for taking him off.
        /// Why?
        /// because we don't need this. whenever a change has to be rolled back,
        /// nested Go returns to higher-level Go, thereby reverting the changes,
        /// since each Go instance on the stack stores its own local version of solution.
        /// ignoring penalty - pTestingMode - is used for checking for impossibilities,
        /// When the program tries generating the Rota day by day,
        /// penalties are irrelevant.
        /// </summary>        
        /// <param Name="pTestingMode">true when you're only testing for impossibilities: ignore penalties and doesn't report progress</param>        
        public int Go(
            Solution pSolution,
            int pShiftID,
            int pEmployeeID,
            bool pTestingMode,
            BackgroundWorker pWorker)
        {            
            if (pSolution.Shifts.Length == 0)
            {
                solutionFinal = pSolution.DeepCopy();                
                solutionFinal.Complete = true;                
            }
            if (solutionFinal.Complete)
            {
                return OUTCOME_COMPLETE;
            }

            // local copy - method-scoped, which is important, because it is recursion! see above
            var local = pSolution.DeepCopy();
            
            if (pShiftID >= 0 &&
                pEmployeeID >= 0)
            {
                EngineAddRemoveShifts.putShiftOn(
                    local,
                    pShiftID,
                    pEmployeeID);
                local.OrphanedShiftsLeft -= 1;
            }

            if (pWorker != null)
            {
                if (pWorker.CancellationPending)
                {
                    return OUTCOME_USER_CANCELLED;
                }
                var percent = (local.Shifts.Length - local.OrphanedShiftsLeft) * 100;
                percent /= local.Shifts.Length;
                pWorker.ReportProgress(
                    percent,
                    new[] 
                        {
                            string.Format("{0}%", percent),
                            Resources.GENERATOR_BuildingSolution
                        });
            }
                        
            // sorts the shifts so that those with least number of candidates are at the top
            var sortedShifts = new Shift[local.Shifts.Length];
            local.Shifts.CopyTo(sortedShifts, 0); // deep copy not needed since Shift is a struct
            Array.Sort(sortedShifts);
            
            #region reacting to the situation when the first one is taken (which means they're all taken, which means we're done)
            /* Shifts whose IsTaken is set to true, are ALWAYS greater than ones which do not.
             * (our implementation of Shift.CompareTo ensures that.)
             * 
             * so, if after we sorted the array, the first shift is taken, it means they all are.
             * and this means we are done - all shifts have found "their man". */
            if (sortedShifts[0].IsTaken)
            {                
                solutionFinal = local.DeepCopy();
                solutionFinal.Complete = true;
                Report report;
                solutionFinal.ComputePenalty(
                    false,
                    out report);
                return OUTCOME_COMPLETE; // exiting successfully
            }
            #endregion

            #region reacting to the situation when the shift has got no Candidates, which means we have to step back
            if (sortedShifts[0].NoCandidates)
            {
                return OUTCOME_HIT_A_DEAD_END;
            }            
            #endregion

            #region getting a list of Candidates for the Shift, and sorting them starting with the least penalized one
            var sortedCandidates = new SortedList<short, int>();
            
            foreach (var candidate in sortedShifts[0].Candidates)
            {
                if (!local[sortedShifts[0].ID, candidate])
                {
                    continue;
                }
                
                short penalty = 0;
                if (!pTestingMode)
                {                           
                    penalty += (short)Penalties.difPenaltyHours(
                        local.Candidates[candidate].Hours,
                        local.Candidates[candidate].ContractHrs,
                        sortedShifts[0].length);
                    penalty += (short)Penalties.difPenaltyFullShiftsInArow(
                        local.Candidates[candidate].FullDays,
                        (byte)sortedShifts[0].Day);
                    penalty += (short)Penalties.difPenaltyWeekend(
                        local.Candidates[candidate].Days,
                        7,
                        (byte)sortedShifts[0].Day);
                    // any additional penalties
                }                    
                /* we cannot duplicate the keys, so we use this as a workaround.
                 * the simpliest possible solution for the problem.
                 * after all: if (for example) 3 employees had the same penalty value,
                 * we would STILL have to try them in some order. */
                while (sortedCandidates.ContainsKey(penalty))
                {
                    penalty++;
                }
                sortedCandidates.Add(penalty, candidate);
            }
            
            #endregion                        
                        
            #region trying those Candidates one by one
            var candidatesEnumerator = sortedCandidates.Values.GetEnumerator();
            var result = OUTCOME_HIT_A_DEAD_END;
                                    
            while (candidatesEnumerator.MoveNext())
            {
                if (result != OUTCOME_HIT_A_DEAD_END)
                {
                    continue;
                }
                // nesting further! recurrency
                result = Go(
                    local,
                    sortedShifts[0].ID,
                    candidatesEnumerator.Current,
                    pTestingMode,
                    pWorker);
                    
                /* Once the solution is found, we don't care about anything anymore.
                 * We just want this "1" (OUTCOME_COMPLETE) to bubble all the way up the stack.
                 * Same thing if user decided to cancel - drop everything, clean up after yourself. */
                if (result == OUTCOME_COMPLETE ||
                    result == OUTCOME_USER_CANCELLED)
                {
                    return result;
                }
            }            
            return OUTCOME_HIT_A_DEAD_END;           
            #endregion
        }
    }
}
